package daybook.model.transaction;

import daybook.model.Account;
import daybook.util.Pair;
import daybook.util.Utils;

import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.*;
import java.util.function.Consumer;
import java.util.function.Function;
import java.util.stream.Collector;

public final class Ledger<T> implements Consumer<Pair<Transaction, TransactionEntry>> {
    private final Map<T, BigDecimal> initialBalances;
    private final Map<T, List<LedgerEntry>> ledger;
    private final LocalDate date;
    private final Function<TransactionEntry, ? extends Iterable<? extends T>> classifier;

    public Ledger(Function<TransactionEntry, ? extends Iterable<? extends T>> classifier, LocalDate date) {
        this.classifier = Objects.requireNonNull(classifier);
        this.date = Objects.requireNonNull(date);
        this.initialBalances = new HashMap<>();
        this.ledger = new HashMap<>();
    }

    public Map<T, List<LedgerEntry>> getLedger() {
        return ledger;
    }

    public Map<T, BigDecimal> getInitialBalances() {
        return initialBalances;
    }

    @Override
    public void accept(Pair<Transaction, TransactionEntry> pair) {
        final TransactionEntry transactionEntry = pair.second();
        if (pair.first().getDate().isBefore(date)) {
            for (T key: classifier.apply(transactionEntry)) {
                initialBalances.merge(key, transactionEntry.getAmount(), BigDecimal::add);
            }
        } else {
            for (T key: classifier.apply(transactionEntry)) {
                final List<LedgerEntry> entries = ledger.computeIfAbsent(key, k -> new ArrayList<>());
                final BigDecimal bal = entries.isEmpty()
                        ? initialBalances.getOrDefault(key, BigDecimal.ZERO)
                        : entries.get(entries.size() - 1).getBalance();
                final LedgerEntry ledgerEntry = ImmutableLedgerEntry.of(pair.first(), transactionEntry, bal.add(transactionEntry.getAmount()));
                entries.add(ledgerEntry);
            }
        }
    }

    private static void append(List<LedgerEntry> entries, List<LedgerEntry> entries2, BigDecimal balance) {
        for (LedgerEntry ledgerEntry : entries2) {
            entries.add(ImmutableLedgerEntry.builder()
                    .from(ledgerEntry)
                    .balance(balance.add(ledgerEntry.getBalance()))
                    .build());
        }
    }

    public void combine(Ledger<T> other) {
        if (ledger.isEmpty()) {
            if (other.ledger.isEmpty()) {
                Utils.mergeInto(initialBalances, other.initialBalances, BigDecimal::add);
            } else {
                for (Map.Entry<T, List<LedgerEntry>> e: other.ledger.entrySet()) {
                    final BigDecimal bal = initialBalances.getOrDefault(e.getKey(), BigDecimal.ZERO);
                    append(ledger.computeIfAbsent(e.getKey(), account -> new ArrayList<>()), e.getValue(), bal);
                }
            }
        } else {
            if (other.ledger.isEmpty()) {
                throw new IllegalStateException();
            } else {
                for (Map.Entry<T, List<LedgerEntry>> e: other.ledger.entrySet()) {
                    final List<LedgerEntry> entries = ledger.computeIfAbsent(e.getKey(), account -> new ArrayList<>());
                    final BigDecimal bal = entries.isEmpty()
                            ? initialBalances.getOrDefault(e.getKey(), BigDecimal.ZERO)
                            : entries.get(entries.size() - 1).getBalance();

                    append(entries, e.getValue(), bal);
                }
            }
        }
    }

    public static <T> Collector<Pair<Transaction, TransactionEntry>, Ledger<T>, Ledger<T>> collector(Function<TransactionEntry, ? extends Iterable<? extends T>> classifier, LocalDate startDate) {
        Objects.requireNonNull(startDate);
        return Collector.of(
                () -> new Ledger<>(classifier, startDate),
                Ledger::accept,
                (left, right) -> {left.combine(right); return left;}
        );
    }

    public static Collector<Pair<Transaction, TransactionEntry>, Ledger<Account>, Ledger<Account>> collector(LocalDate startDate) {
        final Function<TransactionEntry, List<Account>> classifier = item -> Collections.singletonList(item.getAccount());
        return collector(classifier, startDate);
    }
}
